<?php

	namespace org\tekuna\framework\interceptor;

	use \Exception;

	use org\tekuna\base\Tekuna;

	use org\tekuna\core\context\Context;
	use org\tekuna\core\configuration\ConfigurationElement;
	use org\tekuna\core\configuration\ConfigurationException;
	use org\tekuna\core\util\WeightedList;
	use org\tekuna\core\event\Event;
	use org\tekuna\core\event\AbstractEventListener;
	
	use org\tekuna\framework\action\ActionEvent;
	use org\tekuna\framework\action\HttpActionEvent;
	use org\tekuna\framework\info\InformationEvent;
	
	
	/**
	 * This class builds the InvocationChain consisting of Interceptors and the Action
	 * for a certain ActionEvent. At runtime the interceptor chain to use is
	 * detected.
	 */
	class InterceptorProcessor extends AbstractEventListener {

		const
			/** This is the name of the default chain used as fallback */
			TEKUNA_DEFAULT_CHAIN = 'tekunaInterceptorChain',
			
			/** If not defined otherwise, this is the weight for interceptors within the chain */
			DEFAULT_INTERCEPTOR_WEIGHT = 10;
		
		private
			$objContext = NULL,
			$objLogger = NULL,
			$objWeightedInterceptorList = NULL;


		/**
		 * Construct a new InterceptorProcessor. This Constructor registers the new
		 * instance with the EventManager
		 * 
		 * @param Context $objContext the application context
		 */
		public function __construct(Context $objContext) {

			$this -> objContext = $objContext;
			$this -> objLogger = Tekuna :: getLogger(__CLASS__);
			$objContext -> getEventManager() -> registerListener($this);
		}
		
		
		/**
		 * Builds the invocation chain (nested interceptors and the action) for
		 * a given ActionEvent.
		 * 
		 * @param ActionEvent $objActionEvent the current ActionEvent
		 * @return InvocationChain the built chain object
		 */
		public function buildInvocationChain(ActionEvent $objActionEvent) {
			
			// determine interceptor chain name to use
			$sInterceptorChainName = $this -> getInterceptorChainName($objActionEvent);
			
			// build list of interceptors
			$this -> objWeightedInterceptorList = $this -> buildInterceptorList($sInterceptorChainName);
			
			// build invocation chain out of list
			$objChain = new InvocationChain($objActionEvent);
			$this -> addInterceptorsToChain($objChain, $this -> objWeightedInterceptorList);
			return $objChain;
		}
		
		
		private function getInterceptorChainName(ActionEvent $objActionEvent) {
			
			// fallback: default chain of Tekuna
			$sInterceptorChainName = self :: TEKUNA_DEFAULT_CHAIN;
			
			// highest-priority override: at the action
			if ($objActionEvent -> getActionElement() -> hasAttribute('framework', 'interceptor-chain')) {
				
				$sInterceptorChainName = $objActionEvent -> getActionElement() -> getAttribute('framework', 'interceptor-chain') -> getValue();
			}
			
			// other override: at the component
			if ($objActionEvent -> getComponentElement() -> hasAttribute('framework', 'interceptor-chain')) {
				
				$sInterceptorChainName = $objActionEvent -> getComponentElement() -> getAttribute('framework', 'interceptor-chain') -> getValue();
			}
			
			$this -> objLogger -> info("Using interceptor chain '$sInterceptorChainName'");
			return $sInterceptorChainName;
		}

		
		private function buildInterceptorList($sInterceptorChainName, $arrAlreadayIncludedChains = array()) {
			
			$objInterceptorsList = new WeightedList();
			
			// prevent circular includes
			if (in_array($sInterceptorChainName, $arrAlreadayIncludedChains)) {
				
				$this -> objLogger -> warn("Circular inclusion of chain '$sInterceptorChainName' is not possible. Ignoring this include.");
				return $objInterceptorsList;
			}
			$arrAlreadayIncludedChains[] = $sInterceptorChainName;
			
			// find chain definition
			$objChainElement = $this -> findInterceptorChain($sInterceptorChainName);
			
			// load all interceptors (recurse on chain-includes)
			foreach ($objChainElement -> getChildElements() as $objChildElement) {
				
				// use only framework aspects
				if ($objChildElement -> getAspect() != 'framework') {
					
					continue;
				}
				
				// get element weight
				$sWeight = self :: DEFAULT_INTERCEPTOR_WEIGHT;
				if ($objChildElement -> hasAttribute('framework', 'weight')) {
					
					$sWeight = $objChildElement -> getAttribute('framework', 'weight') -> getValue();
				}
				
				// load interceptors
				if ($objChildElement -> getName() == 'interceptor') {
					
					$objInterceptorsList -> addElement($objChildElement, $sWeight);
				}
				
				// load included chains
				if ($objChildElement -> getName() == 'chain-include') {
					
					$sIncludedChainName = $objChildElement -> getAttribute('framework', 'name') -> getValue();
					$objIncludedList = $this -> buildInterceptorList($sIncludedChainName, $arrAlreadayIncludedChains);
					$objInterceptorsList -> addElement($objIncludedList, $sWeight);
				}
			}
			
			// add additional interceptors
			// iterate all top-level interceptors
			$arrTopLevelInterceptors = $this -> objContext -> getConfiguration() -> getRootElement() -> getAllChildElements('framework', 'interceptor');
			foreach ($arrTopLevelInterceptors as $objInterceptorElement) {
				
				// check for their chain reference
				$sChainReference = $objInterceptorElement -> getAttribute('framework', 'chain') -> getValue();

				if ($sChainReference == $sInterceptorChainName) {
					
					// get element weight
					$sWeight = self :: DEFAULT_INTERCEPTOR_WEIGHT;
					if ($objInterceptorElement -> hasAttribute('framework', 'weight')) {
						
						$sWeight = $objInterceptorElement -> getAttribute('framework', 'weight') -> getValue();
					}

					$objInterceptorsList -> addElement($objInterceptorElement, $sWeight);
				}
			}
			
			return $objInterceptorsList;
		}
		
		
		private function findInterceptorChain($sInterceptorChainName) {
			
			foreach ($this -> objContext -> getConfiguration() -> getRootElement() -> getAllChildElements('framework', 'interceptor-chain') as $objChainElement) {

				$sChainName = $objChainElement -> getAttribute('framework', 'name') -> getValue();
				
				if ($sChainName == $sInterceptorChainName) {
					
					return $objChainElement;
				}
			}
			
			throw new ConfigurationException("Interceptor chain '$sInterceptorChainName' does not exist.");
		}

		
		private function addInterceptorsToChain(InvocationChain $objChain, WeightedList $objInterceptorsList) {
			
			foreach ($objInterceptorsList -> getSortedList() as $objInterceptorElement) {
				
				if ($objInterceptorElement instanceof ConfigurationElement) {
					
					$objChain -> appendInterceptor($this -> getInterceptorInstance($objInterceptorElement));
				}
				
				if ($objInterceptorElement instanceof WeightedList) {
					
					$this -> addInterceptorsToChain($objChain, $objInterceptorElement);
				}
			}
		}
		
		
		private function getInterceptorInstance(ConfigurationElement $objInterceptorElement) {
			
		    // get class name
		    $sInterceptorClassName = $objInterceptorElement -> getAttribute('framework', 'class') -> getValue();

		    // check class exists and implicitly trigger class loading
		    if (! class_exists($sInterceptorClassName)) {

		    	throw new ConfigurationException("The interceptor class '$sInterceptorClassName' does not exist.");
		    }
		    
		    // get instance
		    $objInterceptor = new $sInterceptorClassName();

		    // check for the interface
		    if (! $objInterceptor instanceof Interceptor) {

		    	throw new ConfigurationException("The class '$sInterceptorClassName' does not implement the org\\tekuna\\framework\\interceptor\\Interceptor interface.");
		    }
		    
		    // inject dependencies
		    $objInterceptor -> setApplicationContext($this -> objContext);
		    $objInterceptor -> setConfigurationElement($objInterceptorElement);
		    
		    return $objInterceptor;
		}
		
		
		/**
		 * @return boolean returns true of InformationEvents
		 */
		public function handlesEvent(Event $objEvent) {
			
			return $objEvent instanceof InformationEvent;
		}
		
		
		/**
		 * Provides some information about the interceptor chain
		 */
		public function handleEvent(Event $objEvent) {
			
			$sOutput = '';
			$sOutput .= $this -> getInterceptorsInformationRecursive($this -> objWeightedInterceptorList);
			
			$objEvent -> addInfoSection('Interceptor Chain', $sOutput);
		}
		
		
		private function getInterceptorsInformationRecursive(WeightedList $objInterceptorsList, $iLevel = 0) {
			
			$sOutput = '';
			foreach ($objInterceptorsList -> getSortedList(true) as $arrData) {
				
				$sOutput .= str_repeat('  ', $iLevel);
				$sWeight = $arrData['weight'];
				
				if ($arrData['element'] instanceof WeightedList) {
					
					$sOutput .= "  - Included Interceptor Chain (weight: $sWeight)\n";
					$sOutput .= $this -> getInterceptorsInformationRecursive($arrData['element'], $iLevel+1);
				}
				else {
					
					$sClass = $arrData['element'] -> getAttribute('framework', 'class') -> getValue();
					if ($sWeight != 'min' && $sWeight != 'max') {
						
						$sWeight = round((float) $sWeight, 7);
					}
					
					$sOutput .= "  - $sClass (weight: $sWeight)\n";
				}
			}
			
			return $sOutput;
		}
	}